//**********************************************************************
//	Programmer	-	Sujal Bista
//	File Name	-	camera.cpp
//	Description	:	
//					Rendering camera
//
//**********************************************************************

#include "camera.h"

#include <cmath>
#include <math.h>

//constructor
camera::camera(void):
	width(800),	//width of camera
	height(600), //height of camera
	nearPlane(1), //near plane for camera
	farPlane(10000), //far plane for camera
	fovY(45.0f), //The field of view angle, in degrees, in the y-direction
	fovX(0.0f), //The field of view angle, in degrees, in the x-direction
	lookAt(0.0f,0.0f,1.0f), //direction of the camera
	up(0.0f,1.0f,0.0f), //up vector of the camera
	side(1.0f,0.0f,0.0f), //side vector of the camera
	position(0.0f,0.0f,0.0f), //position of the camera
	velocity(0.0f,0.0f,0.0f) //velocity of camera
{
	translateX=0;
	translateY=0;
	expo=0;
	rollVelocity=pitchVelocity=yawVelocity=0;

	projectionMatrix.identity();
	viewMatrix.identity();
}

//set camera axis
void camera::setAxis(const float3& lookAt,const float3& up,const float3& side)
{
	this->lookAt=lookAt; //direction of the camera
	this->up=up; //up vector of the camera
	this->side=side; //side vector of the camera
}

//sets size of the camera
void camera::setSize(const unsigned int& width,const unsigned int& height)
{
	this->width=width;
	this->height=height;
}


//sets FOV near plane and far plane
void camera::setVisibleRange(const float& nearPlane,const float& farPlane,const float& fovY,float fovX)
{
	this->fovY=fovY;
	this->fovX=fovX;
	this->nearPlane=nearPlane;
	this->farPlane=farPlane;
}

//set field of view
void camera::setFOV(const float& fovY,float fovX)
{
	this->fovY=fovY;
	this->fovX=fovX;
}

//gets near plane
float camera::getNearPlane(void) const
{
	return nearPlane;
}


//get far plane
float camera::getFarPlane(void) const
{
	return farPlane;
}


//get field of view in y-axis
float camera::getFOVY(void) const
{
	return fovY;
}

//get field of view in x-axis
float camera::getFOVX(void) const
{
	return fovX;
}

//update the position of the camera
void camera::setPosition(const float3& position)
{
	this->position=position;
}


//sets the camera params based on position, lookat position and up direction left handed
void camera::setLookToLH(const float3& eyePosition, const float3& focusPosition,const float3& upDirection)
{
	//set the position
	position=eyePosition;

	//set look at
	lookAt=(focusPosition-eyePosition);

	//set up
	up=upDirection;

	//compute the side
	side=cross(up,lookAt);

	//set the up vector
	up=cross(lookAt,side);

	//compute view matrix
	computeViewMatrixLH();
}

//sets the camera params based on position, lookat position and up direction right handed
void camera::setLookToRH(const float3& eyePosition, const float3& focusPosition,const float3& upDirection)
{
	//set the position
	position=eyePosition;

	//set look at
	lookAt=(eyePosition-focusPosition);

	//set up
	up=upDirection;

	//compute the side
	side=cross(up,lookAt);

	//set the up vector
	up=cross(lookAt,side);

	//compute view matrix
	computeViewMatrixLH();
}

//moves camera along x axis
void camera::moveX(const float& amount)
{
	velocity+=side*amount;
}

//moves camera along y axis
void camera::moveY(const float& amount)
{
	velocity+=up*amount;
}

//moves camera along z axis
void camera::moveZ(const float& amount)
{
	velocity+=lookAt*amount;
}

//rotate camera along X
void camera::rotateX(const float& theta)
{
	lookAt.rotateX(theta);
	up.rotateX(theta);
}

//rotate camera along Y
void camera::rotateY(const float& theta)
{
	lookAt.rotateY(theta);
	side.rotateY(theta);
}

//rotate camera along Z
void camera::rotateZ(const float& theta)
{
	side.rotateZ(theta);
	up.rotateZ(theta);
}

//rotate camera along camera's X axis
void camera::pitch(const float& theta)
{
	pitchVelocity+=theta;
}

//rotate camera along camera's Y axis
void camera::yaw(const float& theta)
{
	yawVelocity+=theta;
}

//rotate camera along camera's Z axis
void camera::roll(const float& theta)
{
	rollVelocity+=theta;
}

//rotate camera around camera's x axis looking at look at point
void camera::pitch3RDPerson(const float& theta, float lookAtDistance) 
{
	//compute the center
	float3 center=position+lookAt*lookAtDistance;

	//update pitch based on theta as velocity
	lookAt.rotate(theta, side);
	up=cross(lookAt,side);

	//rcomput the position
	position=center-lookAt*lookAtDistance;
}

//rotate camera around camera's y axis looking at look at point
void camera::yaw3RDPerson(const float& theta, float lookAtDistance) 
{
	//compute the center
	float3 center=position+lookAt*lookAtDistance;

	//update yaw based on theta as velocity
	lookAt.rotate(theta, up);
	side=cross(up,lookAt);

	//rcomput the position
	position=center-lookAt*lookAtDistance;
}

//rotate camera around camera's z axis looking at look at point
void camera::roll3RDPerson(const float& theta, float lookAtDistance)
{
	//compute the center
	float3 center=position+lookAt*lookAtDistance;

	//update roll based on theta as velocity
	side.rotate(theta,lookAt);
	up=cross(lookAt,side);

	//rcomput the position
	position=center-lookAt*lookAtDistance;
}

//update the camera
void camera::update(const float& dt)
{
	//update the position
	position+=velocity;

	//pitch
	if(pitchVelocity)
	{
		lookAt.rotate(pitchVelocity,side);
		up=cross(lookAt,side);
	}

	//yaw
	if(yawVelocity)
	{
		lookAt.rotate(yawVelocity,up);
		side=cross(up,lookAt);
	}

	//roll
	if(rollVelocity)
	{
		side.rotate(rollVelocity,lookAt);
		up=cross(lookAt,side);
	}
	
	//update the velocity
	// for smooth transitions
	velocity *= expo;			
	pitchVelocity *= expo;
	yawVelocity *= expo;
	rollVelocity *= expo;

	expo=exp(-4.0f*min(dt,0.010f));

	//compute projection matrix
	computeProjectionMatrixLH();

	//compute view matrix
	computeViewMatrixLH();
}

//destructor
camera::~camera()
{
	
}

//sets cluster param
void camera::setClusterParam(const int &translateX,const int &translateY)
{
	this->translateX=translateX;
	this->translateY=translateY;
}

//compute projection matrix
void camera::computeProjectionMatrixRH(void)
{
	float fovY=(float)this->fovY;
	float fovX=this->fovX;
	
	float ymax = nearPlane * tan(fovY * PI / 360.0f);
	float xmax;
	if(fovX<=0)
		xmax=ymax*((float)width)/height;
	else
		xmax=nearPlane * tan(fovX * PI / 360.0f);
	
	float translateXValue=((xmax)-(-xmax))*translateX;
	float translateYValue=((ymax)-(-ymax))*translateY;

	float left=-xmax+translateXValue;
	float right=xmax+translateXValue;
	float bottom=-ymax+translateYValue;
	float top=ymax+translateYValue;

	projectionMatrix.elements[0]=(float)((2*nearPlane)/(right-left));
	projectionMatrix.elements[1]=0;
	projectionMatrix.elements[2]=(float)((right+left)/(right-left));
	projectionMatrix.elements[3]=0;

	projectionMatrix.elements[4]=0;
	projectionMatrix.elements[5]=(float)((2*nearPlane)/(top-bottom));
	projectionMatrix.elements[6]=(float)((top+bottom)/(top-bottom));
	projectionMatrix.elements[7]=0;

	projectionMatrix.elements[8]=0;
	projectionMatrix.elements[9]=0;
	projectionMatrix.elements[10]=(float)(-(farPlane+nearPlane)/(farPlane-nearPlane));
	projectionMatrix.elements[11]=(float)(-(2*farPlane*nearPlane)/(farPlane-nearPlane));

	projectionMatrix.elements[12]=0;
	projectionMatrix.elements[13]=0;
	projectionMatrix.elements[14]=-1;
	projectionMatrix.elements[15]=0;
}

//compute projection matrix
void camera::computeProjectionMatrixLH(void)
{
	float fovY=(float)this->fovY;
	float fovX=this->fovX;
	
	float ymax = nearPlane * tan(fovY * PI / 360.0f);
	float xmax;
	if(fovX<=0)
		xmax=ymax*((float)width)/height;
	else
		xmax=nearPlane * tan(fovX * PI / 360.0f);
	
	float translateXValue=((xmax)-(-xmax))*translateX;
	float translateYValue=((ymax)-(-ymax))*translateY;

	float left=-xmax+translateXValue;
	float right=xmax+translateXValue;
	float bottom=-ymax+translateYValue;
	float top=ymax+translateYValue;

	projectionMatrix.elements[0]=(float)((2*nearPlane)/(right-left));
	projectionMatrix.elements[1]=0;
	projectionMatrix.elements[2]=(float)((right+left)/(right-left));
	projectionMatrix.elements[3]=0;

	projectionMatrix.elements[4]=0;
	projectionMatrix.elements[5]=(float)((2*nearPlane)/(top-bottom));
	projectionMatrix.elements[6]=(float)((top+bottom)/(top-bottom));
	projectionMatrix.elements[7]=0;

	projectionMatrix.elements[8]=0;
	projectionMatrix.elements[9]=0;
	projectionMatrix.elements[10]=(float)((farPlane+nearPlane)/(farPlane-nearPlane));
	projectionMatrix.elements[11]=(float)(-(2*farPlane*nearPlane)/(farPlane-nearPlane));

	projectionMatrix.elements[12]=0;
	projectionMatrix.elements[13]=0;
	projectionMatrix.elements[14]=1;
	projectionMatrix.elements[15]=0;
}

//compute view matrix left hand
void camera::computeViewMatrixLH(void)
{
	lookAt.unit();
	up.unit();
	side.unit();

	//now update the axis
	viewMatrix.getVector(0)=side;
	viewMatrix.getVector(1)=up;
	viewMatrix.getVector(2)=lookAt;	

	//clear the translation values
	viewMatrix.elements[3]=0.0f;
	viewMatrix.elements[7]=0.0f;
	viewMatrix.elements[11]=0.0f;

	//translate the matrix
	viewMatrix.translate(-position);
}

//compute view matrix right hand
void camera::computeViewMatrixRH(void)
{
	lookAt.unit();
	up.unit();
	side.unit();

	//now update the axis
	viewMatrix.getVector(0)=side;
	viewMatrix.getVector(1)=up;
	viewMatrix.getVector(2)=-lookAt;	

	//clear the translation values
	viewMatrix.elements[3]=0.0f;
	viewMatrix.elements[7]=0.0f;
	viewMatrix.elements[11]=0.0f;

	//translate the matrix
	viewMatrix.translate(-position);
}

//calculate frustum
void camera::updateViewFrustum(void)
{
	frust.calculateFrustum(projectionMatrix*viewMatrix);
}

//draws the camera
void camera::drawCamera(void) const
{
	float fovY=(float)this->fovY;
	float fovX=(float)this->fovX;
	
	float upLength = (float)(tan(fovY * PI / 360.0));
	float sideLength;
	if(fovX<=0)
		sideLength=(float)(upLength*((float)width)/height);
	else
		sideLength=(float)(tan(fovX * PI / 360.0));

	float3 upperLeftFar=position+(lookAt+up*upLength-side*sideLength)*(float)farPlane;
	float3 upperRightFar=position+(lookAt+up*upLength+side*sideLength)*(float)farPlane;
	float3 lowerLeftFar=position+(lookAt-up*upLength-side*sideLength)*(float)farPlane;
	float3 lowerRightFar=position+(lookAt-up*upLength+side*sideLength)*(float)farPlane;

	float3 upperLeftNear=position+(lookAt+up*upLength-side*sideLength)*(float)nearPlane;
	float3 upperRightNear=position+(lookAt+up*upLength+side*sideLength)*(float)nearPlane;
	float3 lowerLeftNear=position+(lookAt-up*upLength-side*sideLength)*(float)nearPlane;
	float3 lowerRightNear=position+(lookAt-up*upLength+side*sideLength)*(float)nearPlane;

	//glPushAttrib(GL_ENABLE_BIT|GL_DEPTH_BUFFER_BIT);


	//	//draw the frame
	//	glBegin(GL_LINES);
	//		glVertex3fv((float *)&upperLeftNear);
	//		glVertex3fv((float *)&upperLeftFar);

	//		glVertex3fv((float *)&upperRightNear);
	//		glVertex3fv((float *)&upperRightFar);

	//		glVertex3fv((float *)&lowerLeftNear);
	//		glVertex3fv((float *)&lowerLeftFar);

	//		glVertex3fv((float *)&lowerRightNear);
	//		glVertex3fv((float *)&lowerRightFar);
	//	glEnd();

	//	// Enable blending
	//	glEnable(GL_BLEND);
	//	//disable culling
	//	glDisable(GL_CULL_FACE);
	//	//disable writing to depth buffer
	//	glDepthMask(false);
	//	// blend function 
	//	glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	//	//draw transparent quads
	//	glBegin(GL_QUADS);
	//		//upper
	//		glVertex3fv((float *)&upperLeftNear);
	//		glVertex3fv((float *)&upperLeftFar);
	//		glVertex3fv((float *)&upperRightFar);
	//		glVertex3fv((float *)&upperRightNear);


	//		//lower
	//		glVertex3fv((float *)&lowerLeftNear);
	//		glVertex3fv((float *)&lowerLeftFar);
	//		glVertex3fv((float *)&lowerRightFar);
	//		glVertex3fv((float *)&lowerRightNear);

	//		//left
	//		glVertex3fv((float *)&upperLeftNear);
	//		glVertex3fv((float *)&upperLeftFar);
	//		glVertex3fv((float *)&lowerLeftFar);
	//		glVertex3fv((float *)&lowerLeftNear);

	//		//right
	//		glVertex3fv((float *)&upperRightNear);
	//		glVertex3fv((float *)&upperRightFar);
	//		glVertex3fv((float *)&lowerRightFar);
	//		glVertex3fv((float *)&lowerRightNear);
	//		
	//	glEnd();


	//	

	//glPopAttrib();

}
//project world coordinate to screen coordinate
float3 camera::worldToScreen(const float3 &world) const
{
	float4 temp=projectionMatrix*(viewMatrix*float4(world.x,world.y,world.z,1.0f));
	if(abs(temp.w)<0.000001f)
	{
		if(temp.w<0)
			temp.w=-0.000001f;
		else
			temp.w=0.000001f;
	}
	float mult=1.0f/temp.w;
	temp*=mult;
	return float3((temp.x*0.5f+0.5f)*width,(temp.y*0.5f+0.5f)*height,(1.0f+temp.z)*0.5f);
}

//project screen coordinate to world coordinate
float3 camera::screenToWorld(const float3 &screen) const
{
	int viewport[4]={0,0,width,height};
	float4x4 viewProjection=projectionMatrix*viewMatrix;
	float4x4 invViewProjection=viewProjection.inverse(); //compute the inverse
	
	float4 temp(screen.x,screen.y,screen.z,1.0);

	//Map x and y from window coordinates
    temp.x = (temp.x - viewport[0]) / viewport[2];
    temp.y = (temp.y - viewport[1]) / viewport[3];

    //Map to range -1 to 1
    temp.x = temp.x * 2 - 1;
    temp.y = temp.y * 2 - 1;
    temp.z = temp.z * 2 - 1;

	float4 worldCoord=invViewProjection*temp;
	worldCoord.x/=worldCoord.w;
	worldCoord.y/=worldCoord.w;
	worldCoord.z/=worldCoord.w;

	return float3(worldCoord.x,worldCoord.y,worldCoord.z);
}

//project world coordinate to camera coordinate
float3 camera::worldToCamera(const float3 &world) const
{
	float4 temp=(viewMatrix*float4(world.x,world.y,world.z,1.0f));
	if(abs(temp.w)<0.000001f)
	{
		if(temp.w<0)
			temp.w=-0.000001f;
		else
			temp.w=0.000001f;
	}
	float mult=1.0f/temp.w;
	temp*=mult;
	return temp;
}

//project camera coordinate to world coordinate
float3 camera::cameraToWorld(const float3 &cameraCoord) const
{
	float4x4 invTranslation(
		1, 0, 0, position.x,
	    0, 1, 0, position.y,
		0, 0, 1, position.z,
		0, 0, 0, 1
	);

	float4x4 invRotation(
		viewMatrix[0],	viewMatrix[4],	viewMatrix[8],	0,
		viewMatrix[1],	viewMatrix[5],	viewMatrix[9],	0,
		viewMatrix[2],	viewMatrix[6],	viewMatrix[10],0,
		0,	0,	0,	viewMatrix[15]
	);

	float4x4 invViewMatrix=invTranslation*invRotation;
	float4 worldCoord=(invViewMatrix*float4(cameraCoord.x,cameraCoord.y,cameraCoord.z,1.0f));
	if(abs( worldCoord.w)<0.000001f)
	{
		if( worldCoord.w<0)
			 worldCoord.w=-0.000001f;
		else
			 worldCoord.w=0.000001f;
	}
	float mult=1.0f/ worldCoord.w;
	worldCoord*=mult;
	return  worldCoord;
}